package simpleftp.client.control;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;

import simpleftp.client.gui.FTPClientWindow;
import simpleftp.ftp.Command;

public class CommandDispatcher implements Runnable 
{
	public static final String 	NL = "\n";
	
	private static final String USER 				= "USER";
	private static final String PASS 				= "PASS";
	private static final String SERVER 				= "SERVER";
	private static final String PORT 				= "PORT";
	private static final String SETTINGS_ROOT 		= "simpleftpsettings";
	private static final String DEFAULT_SETTINGS 	= SETTINGS_ROOT + "/default";
	private static final String LAST_SETTINGS 		= SETTINGS_ROOT + "/last";
	private static final String DEF_USER 			= "paris";
	private static final String DEF_PASS 			= "paris";
	private static final String DEF_SERVER 			= "127.0.0.1";
	private static final String DEF_PORT 			= "7802";
	
	private final ExecutorService 				executorPool;
	private final CompletionService<Boolean> 	completionPool;
	private final FTPClientWindow 				window;
	private final int 							numberOfCores;
	private final double 						blockingCoefficient = 0.9;
	private final int 							poolSize;
	
	private BlockingQueue<Command> 	commands;
	private SimpleFTP 				client;
	private Preferences 			userPrefs;
	private String 					server;
	private String 					port;
	private String 					user;
	private String 					pass;
	private boolean 				alreadyConnected;

	/**
	 * Initialize a commandDispatcher
	 * @param commandQueue The queue where to check for commands
	 * @param window The window of the client GUI
	 */
	public CommandDispatcher(BlockingQueue<Command> commandQueue, final FTPClientWindow window) {
		this.commands 		= commandQueue;
		this.window 		= window;
		alreadyConnected 	= false;
		numberOfCores 		= Runtime.getRuntime().availableProcessors();
		poolSize 			= (int) (numberOfCores / (1 - blockingCoefficient));
		executorPool 		= Executors.newFixedThreadPool(poolSize);
		completionPool	 	= new ExecutorCompletionService<Boolean>(executorPool);
		
		client 				= new SimpleFTP();
		
		// Listener to make the interface more interactive
		MouseListener ml = new MouseAdapter() {
		     public void mousePressed(MouseEvent e) {
	             if(e.getClickCount() == 2) {
	            	 cwd();
	             }
		     }
		 };
		 window.tree.addMouseListener(ml);
	}

	@Override
	public void run() {
		try {
			loadStartupPreferences();
			// Get the last command
			Command cmd = this.commands.take();
			while (!cmd.equals(Command.QUIT)) {
				switch (cmd) {
				case CONNECT:
					connect();
					break;
				case DISCONNECT:
					disconnect();
					break;
				case STOR:
					upload();
					break;
				case LIST:
					listDir();
					break;
				case RETR:
					download();
					break;
				case PWD:
					pwd();
					break;
				case CWD:
					cwd();
					break;
				case CLEAR:
					clearConsole();
					break;
				case LOADPREFS:
					loadPreferences();
					break;
				case SAVEPREFS:
					savePreferences();
					break;
				case DELPREF:
					deletePreference();
					break;
				default:
					break;
				}
				cmd = this.commands.take();
			}
			saveLastPreferences();
			disconnect();
			executorPool.shutdown();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private void clearConsole() {
		window.console.setText("");
	}
	
	/**
	 * Save last preferences
	 */
	private void saveLastPreferences() {
		userPrefs = Preferences.userRoot().node(LAST_SETTINGS);
		userPrefs.put(USER, window.tfUserName.getText());
		userPrefs.put(PASS, new String(window.pfUserPass.getPassword()));
		userPrefs.put(SERVER, window.tfServerName.getText());
		userPrefs.put(PORT, window.tfPort.getText());
		try {
			userPrefs.flush();
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Load last preferences when launching the app
	 */
	private void loadStartupPreferences() {
		try {
			userPrefs = Preferences.userRoot();
			if (!userPrefs.nodeExists(DEFAULT_SETTINGS)) {
				userPrefs = Preferences.userRoot().node(DEFAULT_SETTINGS);
				userPrefs.put(USER, DEF_USER);
				userPrefs.put(PASS, DEF_PASS);
				userPrefs.put(SERVER, DEF_SERVER);
				userPrefs.put(PORT, DEF_PORT);
				userPrefs.flush();
			}
			if (userPrefs.nodeExists(LAST_SETTINGS)) {
				userPrefs = Preferences.userRoot().node(LAST_SETTINGS);
			}
			// Display loaded user preferences
			window.tfUserName.setText(userPrefs.get(USER, DEF_USER));
			window.pfUserPass.setText(userPrefs.get(PASS, DEF_PASS));
			window.tfServerName.setText(userPrefs.get(SERVER, DEF_SERVER));
			window.tfPort.setText(userPrefs.get(PORT, DEF_PORT));
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Choose whether to load default preferences or the last saved
	 */
	private void loadPreferences() {
		try {
			userPrefs = Preferences.userRoot().node(SETTINGS_ROOT);
			String[] userSavedPrefs = userPrefs.childrenNames();
			String s = (String) JOptionPane.showInputDialog(window,
					"Choose preference to load:", "Load Preferences",
					JOptionPane.QUESTION_MESSAGE, null, userSavedPrefs, userSavedPrefs[0]);
			if ((s != null) && (s.length() > 0)) {
				userPrefs = Preferences.userRoot().node(SETTINGS_ROOT + "/" + s);
				window.tfUserName.setText(userPrefs.get(USER, DEF_USER));
				window.pfUserPass.setText(userPrefs.get(PASS, DEF_PASS));
				window.tfServerName.setText(userPrefs.get(SERVER, DEF_SERVER));
				window.tfPort.setText(userPrefs.get(PORT, DEF_PORT));
				
				window.console.append(NL + "User preferences " + s + " loaded.");
			} else {
				window.console.append(NL + "Preferences loading operation cancelled.");
			}
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Save current connection informations
	 */
	private void savePreferences() {
		String s = (String) JOptionPane.showInputDialog(window,
				"Save the current connection info as:", "Save Preferences",
				JOptionPane.QUESTION_MESSAGE, null, null, null);
		if ((s != null) && (s.length() > 0)) {
			userPrefs = Preferences.userRoot().node(SETTINGS_ROOT + "/" + s);
			userPrefs.put(USER, window.tfUserName.getText());
			userPrefs.put(PASS, new String(window.pfUserPass.getPassword()));
			userPrefs.put(SERVER, window.tfServerName.getText());
			userPrefs.put(PORT, window.tfPort.getText());
			try {
				userPrefs.flush();
			} catch (BackingStoreException e) {
				window.console.append(NL + "Could not save preferences: "
						+ e.getMessage());
				e.printStackTrace();
			}
			window.console.append(NL + "Preferences saved as " + s);
		} else {
			window.console.append(NL + "Preferences saving operation cancelled.");
		}
	}
	
	/**
	 * Delete chosen preferences
	 */
	private void deletePreference() {
		try {
			userPrefs = Preferences.userRoot().node(SETTINGS_ROOT);
			String[] userSavedPrefs = userPrefs.childrenNames();
			String s = (String) JOptionPane.showInputDialog(window,
					"Choose preference to delete:", "Delete Preference",
					JOptionPane.QUESTION_MESSAGE, null, userSavedPrefs, userSavedPrefs[0]);
			if ((s != null) && (s.length() > 0)) {
				userPrefs = Preferences.userRoot().node(SETTINGS_ROOT + "/" + s);
				userPrefs.removeNode();
				userPrefs.flush();
				window.console.append(NL + "User preferences " + s + " deleted.");
			} else {
				window.console.append(NL + "Preferences deletion operation cancelled.");
			}
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Download selected files from the FTP server
	 */
	private void download() {
		if(!alreadyConnected)
		{
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		
		TreePath[] treePath = window.tree.getSelectionPaths();
		
		if(treePath != null)
		{
			for(int i=0;i<treePath.length;i++)
			{
				if(treePath[i].toString().equals("[The FTP Server]")) {
					window.console.append(NL + "Cannot download root directory.");
					return;
				}
			}
			// Choose the directory where downloaded file should be stored
			JFileChooser fc = new JFileChooser();
			fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
			fc.setMultiSelectionEnabled(false);
			int returnval = fc.showDialog(window, "Download");
			if( returnval == JFileChooser.APPROVE_OPTION) {
				File destDirectory = fc.getSelectedFile();
				// Download selected files
				List<Future<Boolean>> downloaders = new ArrayList<Future<Boolean>>(treePath.length);
				try {
					for (int i = 0 ; i < treePath.length ; i++) {
						String pathOnServer = treePath[i].toString();
						Downloader downloader = new Downloader(pathOnServer, destDirectory, client.completePwd(), window, server, port, user, pass);
						downloaders.add(completionPool.submit(downloader));
					}
				} catch(IOException e) {
					window.console.append(NL + "Download failed.");
				}
			}
		}
		else {
			window.console.append(NL + "Choose a file to download.");
		}
	}

	/**
	 * Upload a file to the server
	 */
	private void upload() {
		if (!alreadyConnected) {
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		// Choose the files to upload
		final JFileChooser fc = new JFileChooser();
		fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
		fc.setMultiSelectionEnabled(true);
		fc.setDragEnabled(true);
		int returnVal = fc.showDialog(window, "Upload");
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			File[] files = fc.getSelectedFiles();
			// Upload selected files
			List<Future<Boolean>> uploaders = new ArrayList<Future<Boolean>>(files.length);
			try {
				for (File f : files) {
					Uploader uploader = new Uploader(f, client.completePwd(), window, server, port, user, pass);
					uploaders.add(completionPool.submit(uploader));
				}
			} catch(IOException e) {
				window.console.append(NL + "Upload failed");
			}
		} else {
			window.console.append(NL + "Upload action cancelled.");
		}
	}
	
	/**
	 * Display current directory content and update the tree
	 */
	private void listDir() {
		if (!alreadyConnected) {
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		try {	
			String dirContent = client.ls(window.useJson.isSelected());
			window.console.append(NL + dirContent);
			window.root.removeAllChildren();
			DefaultMutableTreeNode userRoot = (DefaultMutableTreeNode) client.getDirectoryTree();
			window.root.add(new DefaultMutableTreeNode(".."));
			window.root.add(userRoot);
			window.tree.updateUI();
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		}
	}
	
	/**
	 * Display current directory name
	 */
	private void pwd() {
		try {
			if (!alreadyConnected) {
				window.console.append(NL + "You are not connected to any server.");
				return;
			}
			String curDir = client.pwd();
			window.console.append(NL + "Current directory on FTP server: " + curDir);
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		}
	}
	
	/**
	 * Change directory to the selected directory on the tree
	 */
	private void cwd() {
		try {
			if (!alreadyConnected) {
				window.console.append(NL + "You are not connected to any server.");
				return;
			}
			String treePath = window.tree.getSelectionPath().toString();
			if(treePath != null) {
				if(treePath.equals("[The FTP Server]")) {
					treePath = "[The FTP Server, home]";
				}
				if(client.cwd(treePath)) {
					listDir();
				}
				else {
					window.console.append(NL + "Couldn't change directory : the selected file might not be a directory.");
				}
			}
			
			} catch (IOException e) {
				window.console.append(NL + e.getMessage());
				e.printStackTrace();
		}
	}
	
	/**
	 * Disconnect the user from the server
	 */
	private void disconnect() {
		try {
			if (alreadyConnected) {
				client.disconnect();
				window.root.removeAllChildren();
				window.tree.updateUI();
				alreadyConnected = false;
				window.console.append(NL + "Now disconnected");
			} else {
				window.console.append(NL + "Already disconnected");
			}
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		}

	}
	
	/**
	 * Connect the user to the server and update the GUI
	 */
	private void connect() {
		if (alreadyConnected) {
			window.console.append(NL
					+ "You are already connected to this server.");
			return;
		}
		server = window.tfServerName.getText();
		if (server == null || "".equals(server)) {
			window.console.append(NL
					+ "You must specify the server IP address.");
			return;
		}
		port = window.tfPort.getText();
		if (port == null || "".equals(port)) {
			window.console.append(NL
					+ "You must specify the server port I must connect to.");
			return;
		}
		user = window.tfUserName.getText();
		if (user == null || "".equals(user))
			user = "anonymous";
		pass = new String(window.pfUserPass.getPassword());
		if (pass == null || "".equals(pass))
			pass = "anonymous";
		try {
			client.connect(server, Integer.parseInt(port), user, pass);
			alreadyConnected = true;
			window.console.append(NL + "Now connected");
			pwd();
			listDir();
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		} catch (NumberFormatException nfe) {
			window.console.append(NL + nfe.getMessage());
			nfe.printStackTrace();
		}
	}
}
